D:\a\csshw\csshw\src\utils\config.rs
Line | Count | Source |
1 | | //! Client and Daemon configuration structs. |
2 | | |
3 | | use serde_derive::{Deserialize, Serialize}; |
4 | | use std::env; |
5 | | use windows::Win32::System::Console::{ |
6 | | BACKGROUND_BLUE, BACKGROUND_INTENSITY, BACKGROUND_RED, FOREGROUND_BLUE, FOREGROUND_GREEN, |
7 | | FOREGROUND_INTENSITY, FOREGROUND_RED, |
8 | | }; |
9 | | |
10 | | /// Behavior when an arrow / `hjkl` keystroke would move the |
11 | | /// enable/disable submenu's selection past the edge of the client grid. |
12 | | #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Copy)] |
13 | | #[serde(rename_all = "snake_case")] |
14 | | pub enum EdgeBehavior { |
15 | | /// Keep the current selection on edge keystrokes (default). |
16 | | Clamp, |
17 | | /// Wrap to the opposite edge of the same row (Left/Right) or column |
18 | | /// (Up/Down). |
19 | | Wrap, |
20 | | } |
21 | | |
22 | | impl Default for EdgeBehavior { |
23 | 77 | fn default() -> Self { |
24 | 77 | return EdgeBehavior::Clamp; |
25 | 77 | } |
26 | | } |
27 | | |
28 | | /// Default console color applied when a client is in the |
29 | | /// `Disabled` state. |
30 | | /// |
31 | | /// Default-grey foreground (red+green+blue, no intensity) on a |
32 | | /// `BACKGROUND_INTENSITY`-only background paints the window as light text on |
33 | | /// a muted dark-grey background - a clear "this client is greyed out" cue |
34 | | /// that stays visually distinct from the daemon's bright-red palette. |
35 | | const DEFAULT_DISABLED_CONSOLE_COLOR: u16 = |
36 | | FOREGROUND_RED.0 | FOREGROUND_GREEN.0 | FOREGROUND_BLUE.0 | BACKGROUND_INTENSITY.0; |
37 | | |
38 | | /// Default console color for the daemon's currently selected submenu |
39 | | /// client: bright-white on blue, distinct from the daemon's bright-red |
40 | | /// and the muted disabled palettes so it stands out at a glance. |
41 | | const DEFAULT_HIGHLIGHTED_CONSOLE_COLOR: u16 = FOREGROUND_RED.0 |
42 | | | FOREGROUND_GREEN.0 |
43 | | | FOREGROUND_BLUE.0 |
44 | | | FOREGROUND_INTENSITY.0 |
45 | | | BACKGROUND_BLUE.0; |
46 | | |
47 | | /// Placeholder for the `<username>@<host>` argument to the chosen SSH program. |
48 | | const DEFAULT_USERNAME_HOST_PLACEHOLDER: &str = "{{USERNAME_AT_HOST}}"; |
49 | | |
50 | | /// Representation of the project configuration. |
51 | | /// |
52 | | /// Includes subcommand specific configurations for `client` and `daemon` subcommands |
53 | | /// as well es the cluster tags. |
54 | | #[derive(Serialize, Deserialize, Default, PartialEq, Debug)] |
55 | | pub struct Config { |
56 | | /// List of cluster tags. |
57 | | /// |
58 | | /// Includes the name of the cluster tag and a list of hostnames. |
59 | | pub clusters: Vec<Cluster>, |
60 | | /// Configuration relevant for the `client` subcommand. |
61 | | pub client: ClientConfig, |
62 | | /// Configuration relevant for the `daemon` subcommand. |
63 | | pub daemon: DaemonConfig, |
64 | | } |
65 | | |
66 | | /// Representation of the project configuration |
67 | | /// where everything is optional. |
68 | | /// |
69 | | /// Used to handle cases where only some or none of the configurations are present. |
70 | | /// Enables backwards compatiblity with configuration files written by older versions. |
71 | | #[derive(Serialize, Deserialize, Default)] |
72 | | pub struct ConfigOpt { |
73 | | #[allow(missing_docs)] |
74 | | pub clusters: Option<Vec<Cluster>>, |
75 | | #[allow(missing_docs)] |
76 | | pub client: Option<ClientConfigOpt>, |
77 | | #[allow(missing_docs)] |
78 | | pub daemon: Option<DaemonConfigOpt>, |
79 | | } |
80 | | |
81 | | impl From<ConfigOpt> for Config { |
82 | | /// Unwraps the existing configuration values or applies the default. |
83 | 15 | fn from(val: ConfigOpt) -> Self { |
84 | 15 | return Config { |
85 | 15 | clusters: val.clusters.unwrap_or_default(), |
86 | 15 | client: val.client.unwrap_or_default().into(), |
87 | 15 | daemon: val.daemon.unwrap_or_default().into(), |
88 | 15 | }; |
89 | 15 | } |
90 | | } |
91 | | |
92 | | impl From<Config> for ConfigOpt { |
93 | | /// Wraps all configuration values as options. |
94 | 1 | fn from(val: Config) -> Self { |
95 | 1 | return ConfigOpt { |
96 | 1 | clusters: Some(val.clusters), |
97 | 1 | client: Some(val.client.into()), |
98 | 1 | daemon: Some(val.daemon.into()), |
99 | 1 | }; |
100 | 1 | } |
101 | | } |
102 | | |
103 | | /// Representation of a cluster tag. |
104 | | #[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq)] |
105 | | pub struct Cluster { |
106 | | /// Name of the cluster tag, used to identify it. |
107 | | pub name: String, |
108 | | /// List of hostnames the cluster tag is an alias for. |
109 | | pub hosts: Vec<String>, |
110 | | } |
111 | | |
112 | | /// Representation of the `client` subcommand configurations. |
113 | | #[derive(Serialize, Deserialize, PartialEq, Debug)] |
114 | | pub struct ClientConfig { |
115 | | /// Full path to the SSH config. |
116 | | /// |
117 | | /// # Example |
118 | | /// |
119 | | /// `'C:\Users\<username>\.ssh\config'` |
120 | | pub ssh_config_path: String, |
121 | | /// Name of the program used to establish the SSH connection. |
122 | | /// # Example |
123 | | /// |
124 | | /// `'ssh'` |
125 | | pub program: String, |
126 | | /// List of arguments provided to the program. |
127 | | /// |
128 | | /// Must include the `username_host_placeholder`. |
129 | | /// |
130 | | /// # Example |
131 | | /// |
132 | | /// `['-XY', '{{USERNAME_AT_HOST}}']` |
133 | | pub arguments: Vec<String>, |
134 | | /// Placeholder string used to inject `<user>@<host>` into the list of arguments. |
135 | | /// |
136 | | /// # Example |
137 | | /// |
138 | | /// `'{{USERNAME_AT_HOST}}'` |
139 | | pub username_host_placeholder: String, |
140 | | /// Controls back- and foreground colors of the client console window |
141 | | /// when the client is in the `Disabled` state. |
142 | | /// |
143 | | /// Uses the same encoding as [`DaemonConfig::console_color`]. |
144 | | /// All [standard Windows color combinations][1] are available: |
145 | | /// |
146 | | /// FOREGROUND_BLUE: 1 \ |
147 | | /// FOREGROUND_GREEN: 2 \ |
148 | | /// FOREGROUND_RED: 4 \ |
149 | | /// FOREGROUND_INTENSITY: 8 \ |
150 | | /// BACKGROUND_BLUE: 16 \ |
151 | | /// BACKGROUND_GREEN: 32 \ |
152 | | /// BACKGROUND_RED: 64 \ |
153 | | /// BACKGROUND_INTENSITY: 128 \ |
154 | | /// |
155 | | /// # Example |
156 | | /// |
157 | | /// Default-grey font on muted dark-grey background: |
158 | | /// 4 + 2 + 1 + 128 = `135` |
159 | | /// |
160 | | /// [1]: https://learn.microsoft.com/en-us/windows/console/console-screen-buffers#character-attributes |
161 | | pub disabled_console_color: u16, |
162 | | /// Controls back- and foreground colors of the client console window |
163 | | /// while it is the currently selected window in the daemon's |
164 | | /// enable/disable submenu. |
165 | | /// |
166 | | /// Uses the same encoding as [`DaemonConfig::console_color`]. |
167 | | /// All [standard Windows color combinations][1] are available: |
168 | | /// |
169 | | /// FOREGROUND_BLUE: 1 \ |
170 | | /// FOREGROUND_GREEN: 2 \ |
171 | | /// FOREGROUND_RED: 4 \ |
172 | | /// FOREGROUND_INTENSITY: 8 \ |
173 | | /// BACKGROUND_BLUE: 16 \ |
174 | | /// BACKGROUND_GREEN: 32 \ |
175 | | /// BACKGROUND_RED: 64 \ |
176 | | /// BACKGROUND_INTENSITY: 128 \ |
177 | | /// |
178 | | /// # Example |
179 | | /// |
180 | | /// Bright-white font on blue background: |
181 | | /// 4 + 2 + 1 + 8 + 16 = `31` |
182 | | /// |
183 | | /// [1]: https://learn.microsoft.com/en-us/windows/console/console-screen-buffers#character-attributes |
184 | | pub highlighted_console_color: u16, |
185 | | } |
186 | | |
187 | | impl Default for ClientConfig { |
188 | | /// Returns a sensible default `ClientConfig`. |
189 | | /// |
190 | | /// # Returns |
191 | | /// |
192 | | /// `ClientConfig` with the following values: |
193 | | /// * `ssh_config_path` - `%USERPROFILE%\.ssh\config` |
194 | | /// * `program` - `ssh` |
195 | | /// * `arguments` - `-XY {{USERNAME_AT_HOST}}` |
196 | | /// * `username_host_placeholder` - `{{USERNAME_AT_HOST}}` |
197 | | /// * `disabled_console_color` - `135` |
198 | | /// * `highlighted_console_color` - `31` |
199 | | /// |
200 | | /// Note: %USERPROFILE% actually is resolved by us, so the actual value |
201 | | /// is whatever the environment variable at runtime points to. |
202 | 78 | fn default() -> Self { |
203 | 78 | return ClientConfig { |
204 | 78 | ssh_config_path: format!("{}\\.ssh\\config", env::var("USERPROFILE").unwrap()), |
205 | 78 | program: "ssh".to_string(), |
206 | 78 | arguments: vec![ |
207 | 78 | "-XY".to_string(), |
208 | 78 | DEFAULT_USERNAME_HOST_PLACEHOLDER.to_string(), |
209 | 78 | ], |
210 | 78 | username_host_placeholder: DEFAULT_USERNAME_HOST_PLACEHOLDER.to_string(), |
211 | 78 | disabled_console_color: DEFAULT_DISABLED_CONSOLE_COLOR, |
212 | 78 | highlighted_console_color: DEFAULT_HIGHLIGHTED_CONSOLE_COLOR, |
213 | 78 | }; |
214 | 78 | } |
215 | | } |
216 | | |
217 | | /// Representation of the `client` subcommand configurations |
218 | | /// where everything is optional. |
219 | | #[derive(Serialize, Deserialize)] |
220 | | pub struct ClientConfigOpt { |
221 | | #[allow(missing_docs)] |
222 | | pub ssh_config_path: Option<String>, |
223 | | #[allow(missing_docs)] |
224 | | pub program: Option<String>, |
225 | | #[allow(missing_docs)] |
226 | | pub arguments: Option<Vec<String>>, |
227 | | #[allow(missing_docs)] |
228 | | pub username_host_placeholder: Option<String>, |
229 | | #[allow(missing_docs)] |
230 | | pub disabled_console_color: Option<u16>, |
231 | | #[allow(missing_docs)] |
232 | | pub highlighted_console_color: Option<u16>, |
233 | | } |
234 | | |
235 | | impl Default for ClientConfigOpt { |
236 | 13 | fn default() -> Self { |
237 | 13 | return ClientConfig::default().into(); |
238 | 13 | } |
239 | | } |
240 | | |
241 | | impl From<ClientConfigOpt> for ClientConfig { |
242 | | /// Unwraps the existing configuration values or applies the default. |
243 | 19 | fn from(val: ClientConfigOpt) -> Self { |
244 | 19 | let default = ClientConfig::default(); |
245 | 19 | return ClientConfig { |
246 | 19 | ssh_config_path: val.ssh_config_path.unwrap_or(default.ssh_config_path), |
247 | 19 | program: val.program.unwrap_or(default.program), |
248 | 19 | arguments: val.arguments.unwrap_or(default.arguments), |
249 | 19 | username_host_placeholder: val |
250 | 19 | .username_host_placeholder |
251 | 19 | .unwrap_or(default.username_host_placeholder), |
252 | 19 | disabled_console_color: val |
253 | 19 | .disabled_console_color |
254 | 19 | .unwrap_or(default.disabled_console_color), |
255 | 19 | highlighted_console_color: val |
256 | 19 | .highlighted_console_color |
257 | 19 | .unwrap_or(default.highlighted_console_color), |
258 | 19 | }; |
259 | 19 | } |
260 | | } |
261 | | |
262 | | impl From<ClientConfig> for ClientConfigOpt { |
263 | | /// Wraps all configuration values as options. |
264 | 14 | fn from(val: ClientConfig) -> Self { |
265 | 14 | return ClientConfigOpt { |
266 | 14 | ssh_config_path: Some(val.ssh_config_path), |
267 | 14 | program: Some(val.program), |
268 | 14 | arguments: Some(val.arguments), |
269 | 14 | username_host_placeholder: Some(val.username_host_placeholder), |
270 | 14 | disabled_console_color: Some(val.disabled_console_color), |
271 | 14 | highlighted_console_color: Some(val.highlighted_console_color), |
272 | 14 | }; |
273 | 14 | } |
274 | | } |
275 | | |
276 | | /// Representation of the `daemon` subcommand configurations. |
277 | | #[derive(Serialize, Deserialize, PartialEq, Debug)] |
278 | | pub struct DaemonConfig { |
279 | | /// Height in pixel of the daemon console window. |
280 | | /// |
281 | | /// Note: we are [DPI Unaware][1] which means the number of pixels |
282 | | /// represents the `logical` scale, not the physical. |
283 | | /// |
284 | | /// [1]: https://learn.microsoft.com/en-us/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows#dpi-unaware |
285 | | pub height: i32, |
286 | | /// Controls how the client console windows make use of the available screen space. |
287 | | /// |
288 | | /// * `> 0.0` - Aims for vertical rectangle shape. |
289 | | /// The larger the value, the more exaggerated the "verticality". |
290 | | /// Eventually the windows will all be columns. |
291 | | /// * `= 0.0` - Aims for square shape. |
292 | | /// * `< 0.0` - Aims for horizontal rectangle shape. |
293 | | /// The smaller the value, the more exaggerated the "horizontality". |
294 | | /// Eventually the windows will all be rows. |
295 | | /// `-1.0` is the sweetspot for mostly preserving a 16:9 ratio. |
296 | | #[serde(alias = "aspect_ratio_adjustement")] |
297 | | pub aspect_ratio_adjustment: f64, |
298 | | /// Controls back- and foreground colors of the daemon console window. |
299 | | /// |
300 | | /// All [standard Windows color combinations][1] are available: |
301 | | /// |
302 | | /// FOREGROUND_BLUE: 1 \ |
303 | | /// FOREGROUND_GREEN: 2 \ |
304 | | /// FOREGROUND_RED: 4 \ |
305 | | /// FOREGROUND_INTENSITY: 8 \ |
306 | | /// BACKGROUND_BLUE: 16 \ |
307 | | /// BACKGROUND_GREEN: 32 \ |
308 | | /// BACKGROUND_RED: 64 \ |
309 | | /// BACKGROUND_INTENSITY: 128 \ |
310 | | /// |
311 | | /// # Example |
312 | | /// |
313 | | /// White font on red background: 8 + 4 + 2 + 1 + 128 + 64 = `207` |
314 | | /// |
315 | | /// [1]: https://learn.microsoft.com/en-us/windows/console/console-screen-buffers#character-attributes |
316 | | pub console_color: u16, |
317 | | /// Behavior when an arrow / `hjkl` keystroke would move the |
318 | | /// enable/disable submenu's selection past the edge of the client grid. |
319 | | /// |
320 | | /// * `clamp` (default) - keep the current selection. |
321 | | /// * `wrap` - wrap to the opposite edge of the same row (Left/Right) |
322 | | /// or column (Up/Down). |
323 | | pub submenu_edge_behavior: EdgeBehavior, |
324 | | } |
325 | | |
326 | | impl Default for DaemonConfig { |
327 | | /// Returns a sensible default `DaemonConfig`. |
328 | | /// |
329 | | /// # Returns |
330 | | /// |
331 | | /// `DaemonConfig` with the following values: |
332 | | /// * `height` - `200` |
333 | | /// * `aspect_ratio_adjustment` - `-1.0` |
334 | | /// * `console_color` - `207` |
335 | | /// * `submenu_edge_behavior` - `clamp` |
336 | 77 | fn default() -> Self { |
337 | 77 | return DaemonConfig { |
338 | 77 | height: 200, |
339 | 77 | aspect_ratio_adjustment: -1f64, |
340 | 77 | console_color: (FOREGROUND_INTENSITY |
341 | 77 | | FOREGROUND_RED |
342 | 77 | | FOREGROUND_GREEN |
343 | 77 | | FOREGROUND_BLUE |
344 | 77 | | BACKGROUND_INTENSITY |
345 | 77 | | BACKGROUND_RED) |
346 | 77 | .0, |
347 | 77 | submenu_edge_behavior: EdgeBehavior::default(), |
348 | 77 | }; |
349 | 77 | } |
350 | | } |
351 | | |
352 | | /// Representation of the `daemon` subcommand configurations |
353 | | /// where everything is optional. |
354 | | #[derive(Serialize, Deserialize)] |
355 | | pub struct DaemonConfigOpt { |
356 | | #[allow(missing_docs)] |
357 | | pub height: Option<i32>, |
358 | | #[allow(missing_docs)] |
359 | | #[serde(alias = "aspect_ratio_adjustement")] |
360 | | pub aspect_ratio_adjustment: Option<f64>, |
361 | | #[allow(missing_docs)] |
362 | | pub console_color: Option<u16>, |
363 | | #[allow(missing_docs)] |
364 | | pub submenu_edge_behavior: Option<EdgeBehavior>, |
365 | | } |
366 | | |
367 | | impl Default for DaemonConfigOpt { |
368 | 12 | fn default() -> Self { |
369 | 12 | return DaemonConfig::default().into(); |
370 | 12 | } |
371 | | } |
372 | | |
373 | | impl From<DaemonConfigOpt> for DaemonConfig { |
374 | | /// Unwraps the existing configuration values or applies the default. |
375 | 19 | fn from(val: DaemonConfigOpt) -> Self { |
376 | 19 | let default = DaemonConfig::default(); |
377 | 19 | return DaemonConfig { |
378 | 19 | height: val.height.unwrap_or(default.height), |
379 | 19 | aspect_ratio_adjustment: val |
380 | 19 | .aspect_ratio_adjustment |
381 | 19 | .unwrap_or(default.aspect_ratio_adjustment), |
382 | 19 | console_color: val.console_color.unwrap_or(default.console_color), |
383 | 19 | submenu_edge_behavior: val |
384 | 19 | .submenu_edge_behavior |
385 | 19 | .unwrap_or(default.submenu_edge_behavior), |
386 | 19 | }; |
387 | 19 | } |
388 | | } |
389 | | |
390 | | impl From<DaemonConfig> for DaemonConfigOpt { |
391 | | /// Wraps all configuration values as options. |
392 | 13 | fn from(val: DaemonConfig) -> Self { |
393 | 13 | return DaemonConfigOpt { |
394 | 13 | height: Some(val.height), |
395 | 13 | aspect_ratio_adjustment: Some(val.aspect_ratio_adjustment), |
396 | 13 | console_color: Some(val.console_color), |
397 | 13 | submenu_edge_behavior: Some(val.submenu_edge_behavior), |
398 | 13 | }; |
399 | 13 | } |
400 | | } |
401 | | |
402 | | #[cfg(test)] |
403 | | #[path = "../tests/utils/test_config.rs"] |
404 | | mod test_config; |